CodeBert для автогенерации комментариев к коду

Код программ отличается от естественного языка из-за его формализма и строгости, однако ничто не мешает воспринимать его как последовательность токенов и работать с ним, как с обычным языком. Существуют исследования, которые показали, что модель BERT, обученная на большом наборе данных, неплохо справляется с некоторыми задачами, связанными с обработкой программного кода. В этом посте я буду решать задачу автогенерации комментариев к нему. Вы узнаете, как подготовить данные для обучения, настроить нейросеть и получить результат.

Данные

Данные представлены в виде набора пар [функция — комментарий] для различных языков программирования (awesome Code Search Net Challenge dataset). Кстати говоря, этот набор изначально был создан не для этой задачи, однако его можно легко перепрофилировать под свои нужды. 

Данные

Цель

public string getlhsbindingtype(final string var)

 { if (this.lhs == null) { return null; } for (int i = 0; i < this.lhs.length; i++) { string type = getlhsbindingtype(this.lhs[i], var); if (type != null) { return type; } } return null; }

get the data-type associated with the binding

Я не стану очищать данные, это описано здесь. Я же буду использовать уже предварительно обработанные данные в объеме 1 % от общего количества образцов в наборе, так как обучение модели занимает довольно много времени. Но, как можно будет убедиться в будущем, генерация комментариев даже на 1 % данных выглядит неплохо. Если у вас есть время и ресурсы, можете обучить модель на всём наборе и получить результаты получше.

CodeBERT

Предварительно обученная модель, которую я буду использовать, взята из статьи исследовательского подразделения Microsoft. В этой модели также использовался набор данных CodeSearchNet, но вместо генерирования комментариев он использовался для обучения модели на основе RoBERTa удобному для восприятия представлению кода и естественного языка. Использование больших языковых моделей для представления текста удобным способом в настоящее время является обычной практикой, поскольку они показали свою эффективность для решения других задач. 

Загрузка, установка и импортирование библиотек

!pip install transformers !git clone -q https://github.com/microsoft/CodeXGLUE.git   import json from dataclasses import dataclass import numpy as np import pandas as pd from transformers import AutoTokenizer

Здесь я прописываю пути до файлов с данными и оборачиваю их в структуру для более удобного дальнейшего использования:

PATH_TO_TRAIN_DATA = '/content/train.csv' PATH_TO_TEST_DATA = '/content/test.csv' PATH_TO_VALIDATION_DATA = '/content/validation.csv' #validation, test and train data_struct = { 'train' : pd.read_csv(PATH_TO_TRAIN_DATA), 'test' : pd.read_csv(PATH_TO_TEST_DATA), 'valid' : pd.read_csv(PATH_TO_VALIDATION_DATA) }

Инициализирую две вспомогательные функции: токенизации текста и записи DataFrame в JSON-файл, так как именно в таком формате требуется подавать данные для модели.

} def write_into_json_file(json_file_name: str, data: pd.DataFrame): ''' json_file_name - name output json file data - pandas data frame write your pandas data to json file ''' with open(json_file_name, 'w') as current_file: for index, current_row in data.iterrows(): current_file.write(json.dumps(current_row.to_dict()) + '\n')   def split_data(split_column: str, new_column: str, data: pd.DataFrame)-> pd.DataFrame: ''' split items in column data - your pandas data frame split_column - column in your pd.df ''' data[new_column] = data[split_column].apply(lambda current_item: current_item.split()) return data

Реализую небольшую предобработку данных с помощью функций, описанных выше:

#preproc data for type_data, value in data_struct.items(): #split target colums code_tokens_step = split_data('code', 'code_tokens', value) docs_tokens_step = split_data('comment', 'docstring_tokens', code_tokens_step) data_struct[type_data] = docs_tokens_step #create json file write_into_json_file(f'/content/{type_data}.jsonl', data_struct[type_data])

Далее создаю конфигурационный класс для модели и на его основе прописываю всю конфигурацию:

@dataclass class ConfigurationModel: learning_rate : float  batch_size : int  beam : int test_file : str source_size : int target_size : int path_to_data_directory : str path_to_output_data_directory : str train_file : str dev_file : str count_epochs : int pretrained_model : str   configuration_codetext_model = ConfigurationModel( learning_rate = 5e-5, batch_size = 8, beam = 10, source_size = 256, target_size = 512, path_to_data_directory = '.', path_to_output_data_directory = 'model_for_java', train_file = '/content/train.jsonl', dev_file = '/content/valid.jsonl', test_file = '/content/test.jsonl', count_epochs = 10, pretrained_model = 'microsoft/codebert-base', ) configuration_codetext_model 

Обучение

Теперь, когда данные обработаны и представлены в удобном формате, можно приступать к обучению. Сделаю этоОбучу модель на обучающей выборке. В качестве метрики использую BLEU-4 (четвёрка означает, что количество словесных n-gram = 4), которая распределена от 0 до 1, но в нашем примере будет использоваться BLEU-4 * 100%. Эта метрика используется в задачах машинного перевода, но и для генерации текста она также хорошо подходит. Если брать задачи машинного перевода, то даже для человека bleu = [0.6:0.7] — отличный результат, потому что каждый человек может перевести текст по-разному. Точности в единицу достигнуть почти невозможно. 

Если посмотреть на исходную задачу, то, во-первых, модель должна сгенерировать текст, а во-вторых, это не просто текст, а осмысленный комментарий к коду. Поэтому ожидать больших значений метрики bleu не стоит. 

#run train model !python /content/CodeXGLUE/Code-Text/code-to-text/code/run.py \ --do_train \ --do_eval \ --do_lower_case \ --model_type roberta \ --model_name_or_path {configuration_codetext_model.pretrained_model} \ --train_filename {configuration_codetext_model.train_file} \ --dev_filename {configuration_codetext_model.dev_file} \ --output_dir {configuration_codetext_model.path_to_output_data_directory} \ --max_source_length {configuration_codetext_model.source_size} \ --max_target_length {configuration_codetext_model.target_size} \ --beam_size {configuration_codetext_model.beam} \ --train_batch_size {configuration_codetext_model.batch_size} \ --eval_batch_size {configuration_codetext_model.batch_size} \ --learning_rate {configuration_codetext_model.learning_rate} \ --num_train_epochs {configuration_codetext_model.count_epochs}

Обучение

После обучения модели её можно проверить на отдельной выборке:

binary_model_file = '/content/model_for_java/checkpoint-best-bleu/pytorch_model.bin' !python  /content/CodeXGLUE/Code-Text/code-to-text/code/run.py \     --do_test \     --model_type roberta \     --model_name_or_path microsoft/codebert-base \     --load_model_path {binary_model_file} \     --dev_filename {configuration_codetext_model.dev_file} \     --test_filename {configuration_codetext_model.test_file} \     --output_dir {configuration_codetext_model.path_to_output_data_directory} \     --max_source_length {configuration_codetext_model.source_size} \     --max_target_length {configuration_codetext_model.target_size} \     --beam_size {configuration_codetext_model.beam} \     --eval_batch_size {configuration_codetext_model.batch_size}

Как можно увидеть, bleu-4 = 11, и это неплохо для такой задачи, даже с учётом того, что bleu в нашем случае распределена от 0 до 100.

Далее считаю получившиеся результаты:

path_to_gold = '/content/model_for_java/test_1.gold' path_to_output = '/content/model_for_java/test_1.output'

Инициализирую функцию считывания из txt-файла:

def read_result_txt_file(txt_file: str)-> list: with open(txt_file) as file:return [' '.join(line.rstrip().replace('\t', ' ').split(' ')[1:]) for line in file]

И для удобства считаю всё в DataFrame:

def read_result_txt_file(txt_file: str)-> list: #true comments and predicted true_sent = read_result_txt_file(path_to_gold) pred_sent = read_result_txt_file(path_to_output) result_data_frame = pd.DataFrame( { 'code' : data_struct['test']['code'], 'true' : true_sent, 'pred' : pred_sent } )   result_data_frame.head(10)

Вывод 10 примеров кода, оригинальных комментариев и комментариев, сгенерированных моделью.

Теперь попробую субъективно сравнить оригинальный комментарий со сгенерированным по шкале от 1 до 5. Code — исходный код, true — исходный комментарий, pred — сгенерированный.

Пример 1:

Code: public t includeas(final class template) { blacklist = false; string[] properties = getallbeanpropertynames(template, false); include(properties); return _this(); }

True: defines included property names as public properties of given template class. sets to black list mode.

Pred: create a new resource

Оценка: 1 — абсолютно непонятно, о чём идёт речь.

Пример 2:

Code: int setdirect(int v0, int v1, int v2, int v3, int v4) { return offset + v0*stride0 + v1*stride1 + v2*stride2 + v3*stride3 + v4*stride4; }

True: experimental : should be package private

Pred: sets the value for the specified point.

Оценка: 4 — исходный комментарий абсолютно никак не отражает функциональность, в отличие от сгенерированного. 

Пример 3:

Code: static private servicetype checkifdap4(string location) throws ioexception { // strip off any trailing dap4 prefix if (location.endswith(«.dap»)) location = location.substring(0, location.length() — «.dap».length()); else if (location.endswith(«.dmr»)) location = location.substring(0, location.length() — «.dmr».length()); else if (location.endswith(«.dmr.xml»)) location = location.substring(0, location.length() — «.dmr.xml».length()); else if (location.endswith(«.dsr»)) location = location.substring(0, location.length() — «.dsr».length()); try (httpmethod method = httpfactory.get(location + «.dmr.xml»)) { int status = method.execute(); if (status == 200) { header h = method.getresponseheader(«content-type»); if ((h != null) && (h.getvalue() != null)) { string v = h.getvalue(); if (v.startswith(«application/vnd.opendap.org»)) return servicetype.dap4; } } if (status == httpstatus.sc_unauthorized ||